package impl

import (
	"context"
	"crypto/sha256"
	"encoding/json"
	"errors"
	"fmt"
	"git.mycaigou.com/gfyx/micro-gfyx-bigdata-service/infrastructure/common"
	contract2 "git.mycaigou.com/gfyx/micro-gfyx-bigdata-service/infrastructure/repository/bigdata/contract"
	contract3 "git.mycaigou.com/gfyx/micro-gfyx-bigdata-service/infrastructure/repository/common/contract"
	"git.mycaigou.com/gfyx/micro-gfyx-bigdata-service/infrastructure/repository/model/bigdata"
	"git.mycaigou.com/gfyx/micro-gfyx-bigdata-service/service/bigdata/contract"
	"git.myscrm.cn/golang/common/ykenv"
	"git.myscrm.cn/golang/stark/v4"
	"github.com/gin-gonic/gin"
	"github.com/gin-gonic/gin/binding"
	"regexp"
	"strconv"
	"strings"
	"sync"
)

type BigdataService struct {
	apiParamRepo contract2.ApiParamRepoIface
	apiRepo      contract2.ApiRepoIface
	srCommonRepo contract3.SrCommonRepoIface
}

func NewBigdataService(
	apiParamRepo contract2.ApiParamRepoIface,
	apiRepo contract2.ApiRepoIface,
	srCommonRepo contract3.SrCommonRepoIface,
) contract.BigdataServiceIface {
	return &BigdataService{
		apiParamRepo: apiParamRepo,
		apiRepo:      apiRepo,
		srCommonRepo: srCommonRepo,
	}
}

// GetDataSet 获取数据集
func (b *BigdataService) GetDataSet(ctx *gin.Context, path string) (map[string]interface{}, error) {
	var (
		// 是否允许分页
		enablePage int32
		//请求方法（GET/POST）
		requestMethod string
		// 查询sql模板
		querySqlTpl string
		// 获取明细的sql
		querySql string
		// 获取总数的sql
		totalSql string
		// 缓存请求参数
		cacheParams *map[string]interface{}
		// 总数
		total   int64
		retData = map[string]interface{}{}
	)

	/*
		1.请求路由鉴权
			a.获取api配置信息（表名：api）
			b.判断请求path和request_path是否一致；如不一致，报错
			c.判断请求方法和request_method是否一致；如不一致，报错
		2.请求参数校验、生成可执行原生sql
			a.获取请求参数配置信息（表名：api_param）
			b.读取请求参数值；如有必填参数未空，报错
			c.sql模板参数替换
			d.分页语句组装；如不分页，默认limit 2000
		3.执行sql,返回结果集
	*/

	// 获取api接口配置信息
	apiInfo, err := b.getApiConfig(ctx.Request.Context(), path, ctx.Request.Method)
	if err != nil {
		return nil, err
	}
	enablePage, requestMethod, querySqlTpl = apiInfo.EnablePage, apiInfo.RequestMethod, apiInfo.QuerySql

	// 获取请求参数配置信息
	paramList, err := b.getApiParamConfig(ctx.Request.Context(), apiInfo.Id, "Request")
	if err != nil {
		return nil, err
	}

	// 如果请求方法是POST,content-type是application/json，需要特殊处理
	if requestMethod == "POST" && ctx.ContentType() == binding.MIMEJSON {
		params := map[string]interface{}{}
		ctx.ShouldBindJSON(&params)
		cacheParams = &params
	}

	for _, item := range paramList {
		// 参数名称
		paramName := item.Name
		// 默认值
		defaultValue := item.DefaultValue
		// 参数类型
		dataType := item.DataType
		// 是否非标准参数
		isNonStandard := item.IsNonStandard

		paramValue := b.getParamValue(ctx, paramName, requestMethod, cacheParams)

		if paramValue == "" {
			// 必填参数缺失，报错
			if item.IsRequired == 1 {
				return nil, fmt.Errorf("%s 是必填参数", paramName)
			}
			paramValue = defaultValue
		}

		// 字符串转义
		paramValue = common.EscapeStr(paramValue)

		if isNonStandard == 0 {
			if dataType == "ARRAY" {
				stringsParamValue := ""
				sliceParamValue := strings.Split(paramValue, ",")
				if len(sliceParamValue) > 0 {
					stringsParamValue = fmt.Sprintf("'%s'", strings.Join(sliceParamValue, "','"))
				}

				expr := fmt.Sprintf("(in( |\\t|\\r|\\n|\\r\\n)*?)(\\(( |\\t|\\r|\\n|\\r\\n)*?(\\${%s})( |\\t|\\r|\\n|\\r\\n)*?\\))", paramName)
				r, _ := regexp.Compile(expr)
				querySqlTpl = r.ReplaceAllString(querySqlTpl, fmt.Sprintf("$1(%s)", stringsParamValue))
			}
		} else {
			/*
				案例
					入参(json格式)： {"title": ["%门%", "%窗%"]}
					sql查询模板：select * from tbl where iterator('title like ${title}', 'or');
					参数替换后：select * from tbl where (title like '%门%') or (title like '%窗%');
			*/
			// 非标准化参数需要做特殊处理
			if dataType == "ARRAY" {
				sliceParamValue := strings.Split(paramValue, ",")

				//expr := fmt.Sprintf("(iterator\\(((.|\\t)+?\\${%s}(.|\\t|\\r|\\n|\\r\\n)*?),(.|\\t|\\r|\\n|\\r\\n)*?('|\")(or|and)('|\")(.|\\t|\\r|\\n|\\r\\n)*?\\))", paramName)
				expr := fmt.Sprintf("(iterator\\(('|\")((.|\\t)+?\\${%s}(.|\\t|\\r|\\n|\\r\\n)*?)('|\"),(.|\\t|\\r|\\n|\\r\\n)*?('|\")(or|and)('|\")(.|\\t|\\r|\\n|\\r\\n)*?\\))", paramName)
				r, _ := regexp.Compile(expr)
				matchStrArr := r.FindAll([]byte(querySqlTpl), -1)
				if matchStrArr != nil {
					for _, item := range matchStrArr {
						s := strings.Split(r.ReplaceAllString(string(item), "$3@-@$9"), "@-@")
						whereConditionTpl, logicalOperator := s[0], s[1]
						//fmt.Printf("whereConditionTpl: %s, logicalOperator: %s \n", whereConditionTpl, logicalOperator)
						whereConditionArr := make([]string, 0)
						for _, paramValue := range sliceParamValue {
							whereCondition := strings.ReplaceAll(whereConditionTpl, "${"+paramName+"}", "'"+paramValue+"'")
							whereConditionArr = append(whereConditionArr, "("+whereCondition+")")
						}
						condition := strings.Join(whereConditionArr, " "+logicalOperator+" ")
						querySqlTpl = strings.ReplaceAll(querySqlTpl, string(item), condition)
					}
				}
			}
		}

		if dataType == "STRING" || dataType == "ARRAY" || dataType == "" {
			paramValue = fmt.Sprintf("'%s'", paramValue)
		}

		// sql模板参数替换
		querySqlTpl = strings.ReplaceAll(querySqlTpl, "${"+paramName+"}", paramValue)
	}

	if enablePage == 1 {
		pageValue := b.getParamValue(ctx, "page", requestMethod, cacheParams)
		pageSizeValue := b.getParamValue(ctx, "page_size", requestMethod, cacheParams)
		page, _ := strconv.Atoi(pageValue)
		pageSize, _ := strconv.Atoi(pageSizeValue)
		if page <= 0 || pageSize <= 0 {
			return nil, errors.New("请填写正确的分页（page/page_size）参数")
		}
		querySql = fmt.Sprintf("%s\n limit %d offset %d", querySqlTpl, pageSize, (page-1)*pageSize)
		totalSql = fmt.Sprintf("select count(1) as total from (\n%s\n) temp", querySqlTpl)
	} else {
		// 没有配置分页，默认最多返回2000条记录
		querySql = fmt.Sprintf("(\n%s\n) limit %d offset %d", querySqlTpl, 2000, 0)
	}

	wg := sync.WaitGroup{}
	errs := make([]error, 0)

	wg.Add(1)
	go func() {
		defer wg.Done()
		// 获取明细数据
		list, err := b.srCommonRepo.GetList(ctx.Request.Context(), querySql)
		if err != nil {
			errs = append(errs, err)
			return
		}
		retData["list"] = list
	}()

	wg.Add(1)
	go func() {
		defer wg.Done()
		// todo: 缓存优化
		if totalSql != "" {
			// 获取total数量
			total, err = b.srCommonRepo.GetTotal(ctx.Request.Context(), totalSql)
			if err != nil {
				errs = append(errs, err)
				return
			}
			retData["total"] = total
		}
	}()

	wg.Wait()

	querySql = strings.ReplaceAll(querySql, "\r", " ")
	querySql = strings.ReplaceAll(querySql, "\n", " ")
	querySql = strings.ReplaceAll(querySql, "\t", " ")

	if len(errs) > 0 {
		return nil, fmt.Errorf("[ERROR REASON] %s | [QUERY SQL] %s", errs[0].Error(), querySql)
	}

	retData["sql"] = querySql
	return retData, nil
}

// GetApiPath 获取api接口配置信息
func (b *BigdataService) getApiConfig(ctx context.Context, path string, requestMethod string) (*bigdata.Api, error) {
	var (
		apiInfo *bigdata.Api
		// 缓存时长：10min
		expireSecond = 600
	)

	redis := stark.RedisConn.Get(ctx)
	defer redis.Close()

	keyName := fmt.Sprintf("bigdata:api_config:%x", sha256.Sum256([]byte(path)))
	cacheValue, _ := redis.Do("get", keyName)
	if cacheValueStr, ok := cacheValue.([]byte); ok {
		json.Unmarshal(cacheValueStr, &apiInfo)
	}

	if apiInfo == nil {
		api, err := b.apiRepo.GetApiInfo(ctx, path)
		if err != nil {
			return nil, err
		}
		apiInfo = api
		apiInfoJson, _ := json.Marshal(apiInfo)

		// 非生产环境，缓存时长设为10s(方便调试)
		if !ykenv.IsProdMode() {
			expireSecond = 10
		}
		redis.Do("set", keyName, string(apiInfoJson), "EX", expireSecond)
	}

	// 校验path和request_method
	if apiInfo.RequestPath != path {
		return nil, errors.New("请求path不存在")
	} else if apiInfo.RequestMethod != requestMethod {
		return nil, errors.New("请求方法不支持")
	} else if apiInfo.QuerySql == "" {
		return nil, errors.New("query_sql未配置")
	}
	return apiInfo, nil
}

// GetApiParam 获取请求参数配置信息
func (b *BigdataService) getApiParamConfig(ctx context.Context, apiId int32, category string) ([]*bigdata.ApiParam, error) {
	var (
		apiParamList []*bigdata.ApiParam
		// 缓存时长：10min
		expireSecond = 600
	)

	redis := stark.RedisConn.Get(ctx)
	defer redis.Close()

	keyName := fmt.Sprintf("bigdata:api_param_config:%d", apiId)
	cacheValue, _ := redis.Do("get", keyName)
	if cacheValueStr, ok := cacheValue.([]byte); ok {
		json.Unmarshal(cacheValueStr, &apiParamList)
	}

	if apiParamList == nil {
		paramList, err := b.apiParamRepo.GetApiParamList(ctx, apiId, category)
		if err != nil {
			return nil, err
		}
		apiParamList = paramList
		apiParamListJson, _ := json.Marshal(apiParamList)

		// 非生产环境，缓存时长设为10s(方便调试)
		if !ykenv.IsProdMode() {
			expireSecond = 10
		}
		redis.Do("set", keyName, string(apiParamListJson), "EX", expireSecond)
	}

	return apiParamList, nil
}

// getParamValue 获取请求参数的value
func (b *BigdataService) getParamValue(ctx *gin.Context, paramName string, requestMethod string, cacheParams *map[string]interface{}) string {
	var paramValue string
	if requestMethod == "POST" {
		if ctx.ContentType() == binding.MIMEMultipartPOSTForm {
			paramValue = ctx.PostForm(paramName)
		} else if ctx.ContentType() == binding.MIMEJSON && cacheParams != nil {
			for key, value := range *cacheParams {
				if key == paramName {
					paramValue = b.assertToString(value)
					break
				}
			}
		}
	} else {
		paramValue = ctx.Query(paramName)
	}
	return paramValue
}

func (b *BigdataService) assertToString(value interface{}) string {
	var paramValue string
	switch v := value.(type) {
	case string:
		paramValue = v
	case int:
		paramValue = strconv.Itoa(v)
	case int8:
		paramValue = strconv.Itoa(int(v))
	case int32:
		paramValue = strconv.Itoa(int(v))
	case int64:
		paramValue = strconv.Itoa(int(v))
	case uint:
		paramValue = strconv.Itoa(int(v))
	case uint8:
		paramValue = strconv.Itoa(int(v))
	case uint32:
		paramValue = strconv.Itoa(int(v))
	case uint64:
		paramValue = strconv.Itoa(int(v))
	case float32:
		paramValue = strconv.Itoa(int(v))
	case float64:
		if v != float64(int(v)) {
			paramValue = fmt.Sprintf("%f", v)
		} else {
			paramValue = strconv.Itoa(int(v))
		}
	case []interface{}:
		s := make([]string, 0)
		for _, item := range v {
			//s = append(s, "'"+b.assertToString(item)+"'")
			s = append(s, b.assertToString(item))
		}
		paramValue = strings.Join(s, ",")
	default:
		paramValue = ""
	}
	return paramValue
}
